package com.ruoyi.jdbc.config;

import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Component;

import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

/**
 * MySQL 分表自动创建工具
 * - 使用模板表复制结构（含索引/约束）：CREATE TABLE IF NOT EXISTS ... LIKE ...
 * - 并发安全：IF NOT EXISTS + 捕获已存在错误
 * - 仅适用于 MySQL（非 H2）
 *
 * 使用前置条件：
 * 1) 目标库已存在
 * 2) 模板表已存在（例如：t_order_tpl）
 */
@Component
@RequiredArgsConstructor
public class MySqlOrderShardTableEnsurer {

    private final JdbcTemplate jdbcTemplate;

    /** 数据库名（例：ry-cloud；支持连字符） */
    private final String database = "ry-cloud";
    /** 模板表名 */
    private final String templateTable = "t_order_tpl";
    /** 分表前缀 */
    private final String logicTablePrefix = "t_order_";

    private static final DateTimeFormatter SUFFIX_FMT = DateTimeFormatter.ofPattern("yyyyMM");

    /** 根据 createTime 确保分表存在（不存在则用模板表复制结构创建） */
    public String ensureByCreateTime(LocalDateTime createTime) {
        Objects.requireNonNull(createTime, "createTime 不能为空（分片键）");
        return ensureForMonth(YearMonth.from(createTime));
    }

    /** 指定月份确保分表存在（例如 2025-06 -> t_order_202506） */
    public String ensureForMonth(YearMonth ym) {
        String suffix = ym.format(SUFFIX_FMT);
        String physical = logicTablePrefix + suffix;
        String fqTarget = bq(database) + "." + bq(physical);
        String fqTemplate = bq(database) + "." + bq(templateTable);

        // ① 先检查模板表是否存在，提前给出更友好的错误
        if (!tableExists(database, templateTable)) {
            throw new IllegalStateException("模板表不存在： " + fqTemplate);
        }

        // ② 并发安全创建：IF NOT EXISTS
        String ddl = "CREATE TABLE IF NOT EXISTS " + fqTarget + " LIKE " + fqTemplate;
        execIgnoreExists(ddl);

        return physical; // 返回物理表名
    }

    /** 批量预建：含起止两端；例如 2025-01 -> 2025-12 */
    public void ensureRange(YearMonth fromIncl, YearMonth toIncl) {
        if (fromIncl.isAfter(toIncl)) throw new IllegalArgumentException("from > to");
        YearMonth cur = fromIncl;
        while (!cur.isAfter(toIncl)) {
            ensureForMonth(cur);
            cur = cur.plusMonths(1);
        }
    }

    /** 预建未来 n 个月（含本月） */
    public void ensureNextMonths(int n) {
        if (n <= 0) return;
        YearMonth cur = YearMonth.now();
        for (int i = 0; i < n; i++) {
            ensureForMonth(cur.plusMonths(i));
        }
    }

    /* -------------------- 内部辅助 -------------------- */

    /** 反引号转义标识符（库/表/列） */
    private String bq(String ident) {
        return "`" + ident.replace("`", "``") + "`";
    }

    /** 判断表是否存在（使用 INFORMATION_SCHEMA） */
    private boolean tableExists(String db, String table) {
        String sql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES " +
                "WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1";
        return Boolean.TRUE.equals(jdbcTemplate.query(sql, (ResultSetExtractor<Object>) rs -> rs.next(), db, table));
    }

    /** 执行 DDL；若并发下别的实例已创建，忽略“已存在”类错误 */
    private void execIgnoreExists(String ddl) {
        try {
            jdbcTemplate.execute(ddl);
        } catch (DataAccessException ex) {
            // 进一步判断是否是“已存在/重复”场景；MySQL 在 IF NOT EXISTS 下通常不会抛错
            // 这里兜底处理某些老版本/代理层的不一致行为
            Throwable root = rootCause(ex);
            if (root instanceof SQLException) {
                String sqlState = ((SQLException) root).getSQLState();   // e.g. 42S01: table exists
                int errorCode   = ((SQLException) root).getErrorCode();  // e.g. 1050  : table exists
                if ("42S01".equals(sqlState) || errorCode == 1050) {
                    return; // 表已存在，忽略
                }
            }
            throw ex;
        }
    }

    private Throwable rootCause(Throwable t) {
        Throwable cur = t;
        while (cur.getCause() != null && cur.getCause() != cur) cur = cur.getCause();
        return cur;
    }
}
